﻿//---------------------------------------------------------------------------
// <copyright file="RibbonDropDownHelper.cs" company="Microsoft Corporation">
//     Copyright (C) Microsoft Corporation.  All rights reserved.
// </copyright>
//---------------------------------------------------------------------------

using Microsoft.Windows.Controls.Ribbon.Primitives;
using MS.Internal;
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls.Primitives;

namespace Microsoft.Windows.Controls.Ribbon
{
	/// <summary>
	///     A static class which defines various helper methods for DropDown Resizing in Ribbon controls.
	/// </summary>
	internal static class RibbonDropDownHelper
	{
		public static void OnPopupResizeStarted(FrameworkElement itemsPresenter)
		{
			if (itemsPresenter != null)
			{
				// We need Height and Width to be a valid double (not NaN or zero)
				// because we are going to add/subtract from these later
				itemsPresenter.Width = itemsPresenter.ActualWidth;
				itemsPresenter.Height = itemsPresenter.ActualHeight;
			}
		}

		public static bool ResizePopup(FrameworkElement itemsPresenter,
			Size minDropDownSize,
			bool canUserResizeHorizontally,
			bool canUserResizeVertically,
			bool isDropDownPositionedLeft,
			bool isDropDownPositionedAbove,
			Rect screenBounds,
			UIElement popupRoot,
			double horizontalDelta,
			double verticalDelta)
		{
			if (itemsPresenter != null)
			{
				verticalDelta = isDropDownPositionedAbove ? (-verticalDelta) : verticalDelta;
				horizontalDelta = isDropDownPositionedLeft ? (-horizontalDelta) : horizontalDelta;

				Rect rootScreenBounds = new Rect(popupRoot.PointToScreen(new Point()),
							  popupRoot.PointToScreen(new Point(popupRoot.RenderSize.Width, popupRoot.RenderSize.Height)));

				// There is a bug in .net3.5 in PopupRoot. When a Popup reaches a screen edge its size is restricted in both dimensions,
				// regardless of which dimension is at the screen edge. 
				// If Popup is at bottom screen edge and you try to increase width horizontally, 
				// the contents increase in size but get clipped by the Popup. This has been fixed in 4.0. 
				// To workaround this bug, we dont allow Popup to ever go beyond top/bottom screen edge via resizing
				// Note that Popup could still hit screen edge even before resizing, when Popup is opened for the first time. 
				// This will cause clipping but we are not going to workaround this case. 

				// Case 1: Popup is already at screen edge.
				// So we want to ignore the delta if trying to increase in direction of screen edge.
				bool isAtBottomScreenEdge = DoubleUtil.GreaterThanOrClose(rootScreenBounds.Bottom, screenBounds.Bottom);
				bool isAtTopScreenEdge = DoubleUtil.LessThanOrClose(rootScreenBounds.Top, screenBounds.Top + TopScreenEdgeBuffer);
				bool isAtRightScreenEdge = DoubleUtil.GreaterThanOrClose(rootScreenBounds.Right, screenBounds.Right);
				bool isAtLeftScreenEdge = DoubleUtil.LessThanOrClose(rootScreenBounds.Left, screenBounds.Left);

				// Case 2: If applying the deltas will make popup go over the screen edge.
				// Truncate delta to whatever is remaining until the screen edge.
				bool isAlmostAtBottomScreenEdge = DoubleUtil.GreaterThanOrClose(rootScreenBounds.Bottom + verticalDelta, screenBounds.Bottom);
				bool isAlmostAtTopScreenEdge = DoubleUtil.LessThanOrClose(rootScreenBounds.Top - verticalDelta, screenBounds.Top + TopScreenEdgeBuffer);
				bool isAlmostAtRightScreenEdge = DoubleUtil.GreaterThanOrClose(rootScreenBounds.Right + horizontalDelta, screenBounds.Right);
				bool isAlmostAtLeftScreenEdge = DoubleUtil.LessThanOrClose(rootScreenBounds.Left - horizontalDelta, screenBounds.Left);

				if (isDropDownPositionedAbove)
				{
					if (isAtTopScreenEdge && DoubleUtil.GreaterThanOrClose(verticalDelta, 0))
					{
						verticalDelta = 0;
					}
					else if (isAlmostAtTopScreenEdge)
					{
						verticalDelta = rootScreenBounds.Top - TopScreenEdgeBuffer;
					}
				}
				else if (!isDropDownPositionedAbove)
				{
					if (isAtBottomScreenEdge && DoubleUtil.GreaterThanOrClose(verticalDelta, 0))
					{
						verticalDelta = 0;
					}
					else if (isAlmostAtBottomScreenEdge)
					{
						verticalDelta = screenBounds.Bottom - rootScreenBounds.Bottom;
					}
				}

				// We need this check because WPF Popup's can render only on single monitor. 
				// (i.e. wont extend to second monitor in multi-mon setup).
				if (isAtRightScreenEdge && DoubleUtil.GreaterThanOrClose(horizontalDelta, 0))
				{
					horizontalDelta = 0;
				}
				else if (isAlmostAtRightScreenEdge)
				{
					horizontalDelta = screenBounds.Right - rootScreenBounds.Right;
				}

				if (isAtLeftScreenEdge && DoubleUtil.GreaterThanOrClose(horizontalDelta, 0))
				{
					horizontalDelta = 0;
				}
				else if (isAlmostAtLeftScreenEdge)
				{
					horizontalDelta = rootScreenBounds.Left - screenBounds.Left;
				}

				double newWidth = itemsPresenter.Width + horizontalDelta;
				double newHeight = itemsPresenter.Height + verticalDelta;
				return ResizePopupActual(itemsPresenter, minDropDownSize, canUserResizeHorizontally, canUserResizeVertically, newWidth, newHeight);
			}

			return false;
		}

		private static bool ResizePopupActual(FrameworkElement itemsPresenter, Size minDropDownSize, bool canUserResizeHorizontally, bool canUserResizeVertically, double newWidth, double newHeight)
		{
			bool result = false;
			if (itemsPresenter != null)
			{
				// Only if the new Width and Height are adequate to display the items in their minimum variant.
				if (canUserResizeHorizontally && DoubleUtil.GreaterThanOrClose(newWidth, 0) &&
					DoubleUtil.LessThanOrClose(minDropDownSize.Width, newWidth))
				{
					itemsPresenter.Width = newWidth;
					result = true;
				}

				if (canUserResizeVertically && DoubleUtil.GreaterThanOrClose(newHeight, 0)
					&& DoubleUtil.LessThanOrClose(minDropDownSize.Height, newHeight))
				{
					itemsPresenter.Height = newHeight;
					result = result & true;
				}
			}

			return result;
		}

		/// <summary>
		/// Cache the screen bounds of the monitor in which the targetElement is rendered.
		/// </summary>
		/// <param name="itemsPresenter"></param>
		/// <returns></returns>
		public static Rect GetScreenBounds(FrameworkElement targetElement, Popup popup)
		{
			if (targetElement != null)
			{
				Rect targetBoundingBox = new Rect(targetElement.PointToScreen(new Point()),
											  targetElement.PointToScreen(new Point(targetElement.RenderSize.Width, targetElement.RenderSize.Height)));

				NativeMethods.RECT rect = new NativeMethods.RECT() { top = 0, bottom = 0, left = 0, right = 0 };
				NativeMethods.RECT nativeBounds = NativeMethods.FromRect(targetBoundingBox);

				IntPtr monitor = NativeMethods.MonitorFromRect(ref nativeBounds, NativeMethods.MONITOR_DEFAULTTONEAREST);
				if (monitor != IntPtr.Zero)
				{
					NativeMethods.MONITORINFOEX monitorInfo = new NativeMethods.MONITORINFOEX();

					monitorInfo.cbSize = Marshal.SizeOf(typeof(NativeMethods.MONITORINFOEX));
					NativeMethods.GetMonitorInfo(new HandleRef(null, monitor), monitorInfo);

					// WPF Popup special cases MenuItem to be restricted to work area
					// Hence Ribbon applies the same rules as well. 
					if (popup.TemplatedParent is RibbonMenuItem)
					{
						rect = monitorInfo.rcWork;
					}
					else if (popup.TemplatedParent is RibbonMenuButton)
					{
						rect = monitorInfo.rcMonitor;
					}
				}

				return NativeMethods.ToRect(rect);
			}

			return Rect.Empty;
		}

		public static void ClearLocalValues(FrameworkElement itemsPresenter, Popup popup)
		{
			if (itemsPresenter != null)
			{
				itemsPresenter.ClearValue(FrameworkElement.HeightProperty);
				itemsPresenter.ClearValue(FrameworkElement.WidthProperty);
			}
			if (popup != null)
			{
				popup.ClearValue(Popup.PlacementProperty);
				popup.ClearValue(Popup.VerticalOffsetProperty);
				popup.ClearValue(Popup.HorizontalOffsetProperty);
			}
		}

		public static Size GetMinDropDownSize(RibbonMenuItemsPanel itemsHost, Popup popup, Thickness borderThickness)
		{
			Size minSize = new Size();
			if (itemsHost != null)
			{
				minSize = itemsHost._cachedAutoSize;
				if (popup != null)
				{
					FrameworkElement popupChild = popup.Child as FrameworkElement;
					if (popupChild != null && DoubleUtil.GreaterThan(popupChild.MinWidth, minSize.Width))
					{
						// MenuButton's BorderThickness around the ItemsPresenter
						minSize.Width = popupChild.MinWidth - (borderThickness.Left + borderThickness.Right);
					}
				}
			}

			return minSize;
		}

		// This const was deduced empirically. Popup leaves this much gap between the top of the screen edge and itself.
		private const double TopScreenEdgeBuffer = 5.0;
	}
}
